/*
MockO: FuDePAN's Mocking Operations
    Copyright (C) 2012  Daniel Gutson, FuDePAN

    This file is part of MockO.

    MockO is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    MockO is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with MockO.  If not, see <http://www.gnu.org/licenses/>.

    This is the main tool file.
*/

#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <list>
#include <set>
#include "getoptpp/getopt_pp.h"
using namespace std;
using namespace GetOpt;

struct MockoArguments
{
    bool valgrindMode;
    unsigned int valgrindPid;
    bool generateGdbscript;
    bool generateHeader;
    bool generateCpp;
    bool showHelp;
    string mocklist;
    
    MockoArguments():
        valgrindMode(false),
        valgrindPid(0u),
        generateGdbscript(true),
        generateHeader(true),
        generateCpp(true),
        showHelp(false)
    {}
};

static void doWork(const MockoArguments& mockoArgs);
static void parseArguments(int argc, char* argv[], MockoArguments& args);
static void showHelp(const char* progName);

int main(int argc, char* argv[])
{
    int ret = EXIT_FAILURE;
    try
    {
        MockoArguments args;
        parseArguments(argc, argv, args);

        if (args.showHelp)
            showHelp(argv[0]);

        doWork(args);
        ret = EXIT_SUCCESS;
    }
    catch (const GetOptEx& ex)
    {
        showHelp(argv[0]);
    }
    catch(const char* err)
    {
        cerr << err << endl;
    }
    catch(const string& err)
    {
        cerr << err << endl;
    }
    catch (...)
    {
        cerr << "Unknown error" << endl;
    }
    return ret;
}

static void writeCommonHeadingFile(ofstream& of)
{
    of << "//DO NOT EDIT. Generated with MockO.\n" << endl;
}

class FunctionToMock
{
    static bool replaceChar(string& type, char c, const string& expansion)
    {
        const string::size_type atPos = type.find(c);
        const bool found = atPos != string::npos;
        if (found)
            type.replace( atPos, 1, expansion);

        return found;
    }

    static void applyCVQualifiers(string& type)
    {
        bool foundConst;
        bool foundVolatile;
        do
        {
            foundConst    = replaceChar(type, '@', " const ");
            foundVolatile = replaceChar(type, '#', " volatile ");
        }
        while (foundConst || foundVolatile);
    }

    struct Arg
    {
        string type;
        string name;
    };
    list<Arg> args;

    static set<string> keywordsToIgnore;

    static bool isKeywordToIgnore(const string& type)
    {
      // first check to load keywords: only C-keywords
      if (keywordsToIgnore.empty())
      {
        keywordsToIgnore.insert("enum");
        keywordsToIgnore.insert("struct");
        keywordsToIgnore.insert("union");
      }

      return keywordsToIgnore.count(type) > 0;
    }

public:
    string ret;
    string name;

    void parse(const string& line)
    {
        args.clear();
        stringstream ss(line);
        if (!(ss >> ret))
            throw "Return type not found in line '" + line + "'";

        if (!(ss >> name))
            throw "Function name not found in line '" + line + "'";

        applyCVQualifiers(ret);

        Arg arg;
        bool isVoid = false;
        while (ss >> arg.type && !isVoid)
        {
            const bool ignoredKeyword = isKeywordToIgnore(arg.type);
            if (ignoredKeyword)
            {
                if (!(ss >> arg.type))
                    throw "Argument type missing in line '" + line + "'";
            }

            isVoid = (arg.type == "void");
            if (isVoid)
            {
                if (ignoredKeyword)
                    throw "Invalid use of 'void' in line '" + line + "'";

                // just do checkings:
                if (!args.empty())
                    throw "Invalid type 'void' in function in line '" + line + "'";

                if (ss >> arg.name)
                    throw "Extra arguments in void function in line '" + line + "'";
            }
            else
            {
                applyCVQualifiers(arg.type);
                if (!(ss >> arg.name))
                    throw "Argument name missing in line '" + line + "'";

                args.push_back(arg);
            }
        }
    }

    string enableFunctionName() const
    {
        return "enableMock_" + name;
    }

    string enableFunctionPrototype() const
    {
        return "void " + enableFunctionName() + "(void)";
    }

    string enableTimesFunctionName() const
    {
        return "enableMockTimes_" + name;
    }

    string enableTimesFunctionPrototype() const
    {
        return "void " + enableTimesFunctionName() + "(size_t ignores, size_t atMost)";
    }

    string disableFunctionName() const
    {
        return "disableMock_" + name;
    }

    string disableFunctionPrototype() const
    {
        return "void " + disableFunctionName() + "(void)";
    }

    string getArgsString(bool types, bool names) const
    {
        string strArgs;
        for (list<Arg>::const_iterator arg = args.begin(); arg != args.end(); ++arg)
        {
            if (arg != args.begin())
                strArgs += ", ";

            if (types)
            {
                strArgs += arg->type;
                if (names)
                    strArgs += " ";
            }

            if (names)
                strArgs += arg->name;
        }

        return strArgs;
    }

    string bindingFunction() const
    {
        string function = ret + " " + name + "_gdb(" + getArgsString(true, true) + ")\n";
        function += "{\n";
        function += "    return mocko." + name + "(" + getArgsString(false, true) + ");\n";
        function += "}\n";
        return function;
    }

    string bindingFunctionCall() const
    {
        const string funcCall = name + "_gdb(" + getArgsString(false, true) + ")";
        if ( ret == "void" )
            return "call " + funcCall + "\n" + "return";
        else
            return "return " + funcCall;
    }

    string mockMethod() const
    {
        stringstream ss;
        ss << "MOCK_METHOD" << args.size() << "(" << name << ", " << ret << "(" << getArgsString(true, false) << "));";
        return ss.str();
    }

    string bpName() const
    {
        return "$bp" + name;
    }

    string timesName() const
    {
        return "$times" + name;
    }

    string topName() const
    {
        return "$top" + name;
    }
};
set<string> FunctionToMock::keywordsToIgnore;

typedef list<FunctionToMock> Functions;

static void parseMockListFile(const string& inputFile, Functions& functions, string& preamble)
{
    ifstream listFile(inputFile.c_str());
    if (!listFile)
        throw "mocklist file not found";

    string functionToMockLine;
    while (getline(listFile, functionToMockLine))
    {
        if (!functionToMockLine.empty())
        {
            // distinguish between preamble lines and function lines
            if (functionToMockLine[0] == ';')
            {
                if (!functions.empty())
                    throw "Preamble lines have to be at the beginning of the mock list file.";
                preamble += functionToMockLine.substr(1) + "\n";
            }
            else
            {
                functions.push_back(FunctionToMock());
                functions.back().parse(functionToMockLine);
            }
        }
    }
}

static void writeHeaderFile(ofstream& of, const Functions& functions, const string& preamble)
{
    writeCommonHeadingFile(of);

    of << preamble << endl;

    of << "#include <gmock/gmock.h>\n\n"
          "struct MockObject\n"
          "{\n";

    if ( functions.empty() )
    {
      FunctionToMock dummy;
      dummy.parse("void dummy void");
      of << "    " << dummy.mockMethod() << endl;
    }
    else
    {
      for ( Functions::const_iterator func = functions.begin(); func != functions.end(); ++func)
      {
          of << "    " << func->mockMethod() << endl;
      }
    }

    // write the singleton stuff:
    of << "\n"
          "    static MockObject* instance()\n"
          "    {\n"
          "        if ( _instance == NULL )\n"
          "            _instance = new MockObject;\n"
          "        return _instance;\n"
          "    }\n"
          "\n"
          "    static void destroy()\n"
          "    {\n"
          "        delete _instance;\n"
          "        _instance = NULL;\n"
          "    }\n"
          "private:\n"
          "    static MockObject* _instance;\n"
          "};\n"
          "#define mocko (*MockObject::instance())\n\n" << endl;

    for ( Functions::const_iterator func = functions.begin(); func != functions.end(); ++func)
    {
        of << func->enableFunctionPrototype() << ";\n" << endl;
        of << func->enableTimesFunctionPrototype() << ";\n" << endl;
        of << func->disableFunctionPrototype() << ";\n" << endl;
    }
}

static void writeCppFile(ofstream& of, const Functions& functions)
{
    writeCommonHeadingFile(of);
    of << "#include \"mocko_bind.h\"\n\n"
          "MockObject* MockObject::_instance = NULL;\n\n";

    for ( Functions::const_iterator func = functions.begin(); func != functions.end(); ++func)
    {
        of << func->enableTimesFunctionPrototype() << "\n{}\n" << endl;
        of << func->enableFunctionPrototype() << "\n"
              "{\n"
              "    " << func->enableTimesFunctionName() << "(0, 0);\n"
              "}\n"
           << endl;
        of << func->disableFunctionPrototype() << "\n{}\n" << endl;
        of << func->bindingFunction() << endl;
    }

    // write pre/post declarations:
    of << "extern void mocko_pre_tests(void) __attribute__ ((weak));\n"
          "extern void mocko_post_tests(void) __attribute__ ((weak));\n" << endl;

    // write main:
    of << "int main(int argc, char **argv)\n"
          "{\n"
          "    ::testing::InitGoogleTest(&argc, argv);\n"
          "    if (mocko_pre_tests != NULL)\n"
          "        mocko_pre_tests();\n"
          "    const int ret = RUN_ALL_TESTS();\n"
          "    if (mocko_post_tests != NULL)\n"
          "        mocko_post_tests();\n"
          "    mocko.destroy();\n"
          "    return ret;\n"
          "}\n";
}

static void writeGdbFile(ofstream& of, const Functions& functions, const MockoArguments& mockoArgs)
{
    of << "set height 0" << endl;
    for ( Functions::const_iterator func = functions.begin(); func != functions.end(); ++func)
    {
        //instrument real function:
        of << "br " << func->name << endl
           << "set " << func->bpName() << " = $bpnum" << endl
           << "commands" << endl
           << "silent" << endl
           << "set " << func->timesName() << " = " << func->timesName() << " + 1" << endl
           << "if " << func->timesName() << " == " << func->topName() << endl
           << "disable br " << func->bpName() << endl
           << "end" << endl
           << func->bindingFunctionCall() << endl
           << "cont" << endl
           << "end" << endl
           << "disable br " << func->bpName() << endl;

        //add enabler:
        of << "br " << func->enableTimesFunctionName() << endl
           << "commands" << endl
           << "silent" << endl
           << "if ignores > 0" << endl
           << "ignore " << func->bpName() << " ignores" << endl
           << "end" << endl
           << "set " << func->timesName() << " = 0" << endl
           << "set " << func->topName() << " = atMost" << endl
           << "enable br " << func->bpName() << endl
           << "cont" << endl
           << "end" << endl;

        //add disabler:
        of << "br " << func->disableFunctionName() << endl
           << "commands" << endl
           << "silent" << endl
           << "disable br " << func->bpName() << endl
           << "cont" << endl
           << "end" << endl;
    }

    if (mockoArgs.valgrindMode)
    {
        of << "target remote | vgdb" << endl
           << "continue" << endl;
    }
    else if (mockoArgs.valgrindPid > 0u)
    {
        of << "target remote | vgdb --pid=" << mockoArgs.valgrindPid << endl
           << "continue" << endl;
    }
    else
    {
        // write footer:
        of << "run" << endl
           << "quit" << endl;
    }
}

void doWork(const MockoArguments& mockoArgs)
{
    Functions functions;
    string preamble;
    parseMockListFile(mockoArgs.mocklist, functions, preamble);

    // open three files: mocko_bind.cpp, mocko_bind.h and mocko_bind.gdb
    // and for each function to mock, write to each file

    if (mockoArgs.generateCpp)
    {
        ofstream mocko_cpp("mocko_bind.cpp");
        writeCppFile(mocko_cpp, functions);
    }

    if (mockoArgs.generateHeader)
    {
        ofstream mocko_h("mocko_bind.h");
        writeHeaderFile(mocko_h, functions, preamble);
    }

    if (mockoArgs.generateGdbscript)
    {
        ofstream mocko_gdb("mocko_bind.gdb");
        writeGdbFile(mocko_gdb, functions, mockoArgs);
    }
}

void parseArguments(int argc, char* argv[], MockoArguments& args)
{
    GetOpt_pp ops(argc, argv);

    ops.exceptions_all();

    ops >> OptionPresent('v', "valgrind", args.valgrindMode);

    if (ops >> OptionPresent('p', "valgrind-pid"))
    {
        if (args.valgrindMode)
            throw "Either specify --valgrind or --valgrind-pid, but not both.";

        ops >> Option('p', "valgrind-pid", args.valgrindPid);
    }

    ops >> Option('f', "mock-list", args.mocklist);
    args.generateGdbscript = !(ops >> OptionPresent("no-script"));
    args.generateHeader = !(ops >> OptionPresent("no-header"));
    args.generateCpp = !(ops >> OptionPresent("no-cpp"));
    ops >> OptionPresent('h', "help", args.showHelp);

    if (!(args.generateGdbscript || args.generateHeader || args.generateCpp))
        throw "At least one output has to be generated. "
              "You specified no-* meaning no outputs. ID-10T Error.";

    ops.end_of_options();
}

void showHelp(const char* progName)
{
    cerr << "Usage: " << progName << endl <<
            "\t-f | --mocklist" <<  " mocklistFile" << endl <<
            "\t[-v | --valgrind]" << endl <<
            "\t[-p | --valgrind-pid PID]" << endl <<
            "\t[--no-script] [--no-cpp] [--no-header]" << endl <<
            "\t[-h | --help]" << endl;
}

